package org.farng.mp3.id3;

import org.farng.mp3.AbstractMP3Tag;
import org.farng.mp3.TagConstant;
import org.farng.mp3.TagException;
import org.farng.mp3.TagNotFoundException;
import org.farng.mp3.TagUtility;

import java.io.IOException;
import java.io.RandomAccessFile;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;

/**
 * Title: ID3v2 Description: This is the abstract base class for all ID3v2 tags
 * Copyright: Copyright (c) 2002 Company:
 * 
 * @author Eric Farng
 * @version $Revision: 1.5 $
 */
public abstract class AbstractID3v2 extends AbstractID3 {
	// just used to add up the padding.
	// the size written to the file is determined by file pointer

	/** DOCUMENT ME! */
	static public int paddingCounter = 0;

	/** DOCUMENT ME! */
	protected HashMap frameMap = null;

	/** DOCUMENT ME! */
	protected String duplicateFrameId = "";

	/** DOCUMENT ME! */
	protected byte majorVersion = 0;

	/** DOCUMENT ME! */
	protected byte revision = 0;

	/** DOCUMENT ME! */
	protected int duplicateBytes = 0;

	/** DOCUMENT ME! */
	protected int emptyFrameBytes = 0;

	/** DOCUMENT ME! */
	protected int fileReadSize = 0;

	/** DOCUMENT ME! */
	protected int invalidFrameBytes = 0;

	/** DOCUMENT ME! */
	protected int padding = 0;

	public AbstractID3v2() {
	}

	
	public AbstractID3v2(AbstractID3v2 copyObject) {
		super(copyObject);
		this.duplicateFrameId = new String(copyObject.duplicateFrameId);
		this.majorVersion = copyObject.majorVersion;
		this.revision = copyObject.revision;
		this.duplicateBytes = copyObject.duplicateBytes;
		this.emptyFrameBytes = copyObject.emptyFrameBytes;
		this.fileReadSize = copyObject.fileReadSize;
		this.invalidFrameBytes = copyObject.invalidFrameBytes;
		this.padding = copyObject.padding;

		Iterator iterator = copyObject.frameMap.keySet().iterator();
		String identifier;
		AbstractID3v2Frame newFrame;

		while (iterator.hasNext()) {
			identifier = (String) iterator.next();
			newFrame = (AbstractID3v2Frame) TagUtility
					.copyObject(copyObject.frameMap.get(identifier));
			this.frameMap.put(newFrame.getIdentifier(), newFrame);
		}
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	public int getDuplicateBytes() {
		return duplicateBytes;
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	public String getDuplicateFrameId() {
		return duplicateFrameId;
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	public int getEmptyFrameBytes() {
		return emptyFrameBytes;
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	public int getFileReadBytes() {
		return fileReadSize;
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param frame
	 *            DOCUMENT ME!
	 */
	public void setFrame(AbstractID3v2Frame frame) {
		frameMap.put(frame.getIdentifier(), frame);
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param identifier
	 *            DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	public AbstractID3v2Frame getFrame(String identifier) {
		return (AbstractID3v2Frame) frameMap.get(identifier);
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	public int getFrameCount() {
		if (frameMap == null) {
			return 0;
		} else {
			return frameMap.size();
		}
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param identifier
	 *            DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	public Iterator getFrameOfType(String identifier) {
		Iterator iterator = frameMap.keySet().iterator();
		HashSet result = new HashSet();
		String key;

		while (iterator.hasNext()) {
			key = (String) iterator.next();

			if (key.startsWith(identifier)) {
				result.add(frameMap.get(key));
			}
		}

		return result.iterator();
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	public int getInvalidFrameBytes() {
		return invalidFrameBytes;
	}

	// above is all just debug information
	public int getPaddingSize() {
		return padding;
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param tag
	 *            DOCUMENT ME!
	 */
	public void append(AbstractMP3Tag tag) {
		AbstractID3v2 oldTag = this;
		AbstractID3v2 newTag = null;

		if (tag != null) {
			if (tag instanceof AbstractID3v2) {
				newTag = (AbstractID3v2) tag;
			} else {
				newTag = new ID3v2_4(tag);
			}

			Iterator iterator = newTag.frameMap.values().iterator();
			AbstractID3v2Frame frame;

			while (iterator.hasNext()) {
				frame = (AbstractID3v2Frame) iterator.next();

				if (oldTag.hasFrame(frame.getIdentifier()) == false) {
					oldTag.setFrame(frame);
				}
			}
		}

		// super.append(newTag);
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param file
	 *            DOCUMENT ME!
	 * 
	 * @throws IOException
	 *             DOCUMENT ME!
	 * @throws TagException
	 *             DOCUMENT ME!
	 */
	public void append(RandomAccessFile file) throws IOException, TagException {
		AbstractID3v2 oldTag;

		try {
			oldTag = new ID3v2_4(file);
			oldTag.append(this);
			oldTag.write(file);
		} catch (TagNotFoundException ex) {
			try {
				oldTag = new ID3v2_3(file);
				oldTag.append(this);
				oldTag.write(file);
			} catch (TagNotFoundException ex2) {
				try {
					oldTag = new ID3v2_2(file);
					oldTag.append(this);
					oldTag.write(file);
				} catch (TagNotFoundException ex3) {
					this.write(file);
				}
			}
		}
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param file
	 *            DOCUMENT ME!
	 * 
	 * @throws IOException
	 *             DOCUMENT ME!
	 */
	public void delete(RandomAccessFile file) throws IOException {
		// this works by just erasing the "TAG" tag at the beginning
		// of the file
		byte[] buffer = new byte[3];

		if (seek(file)) {
			file.seek(0);
			file.write(buffer);
		}
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param obj
	 *            DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	public boolean equals(Object obj) {
		if ((obj instanceof AbstractID3v2) == false) {
			return false;
		}

		AbstractID3v2 object = (AbstractID3v2) obj;

		if (this.frameMap.equals(object.frameMap) == false) {
			return false;
		}

		return super.equals(obj);
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param identifier
	 *            DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	public boolean hasFrame(String identifier) {
		return frameMap.containsKey(identifier);
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param identifier
	 *            DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	public boolean hasFrameOfType(String identifier) {
		Iterator iterator = frameMap.keySet().iterator();
		String key;
		boolean found = false;

		while (iterator.hasNext() && !found) {
			key = (String) iterator.next();

			if (key.startsWith(identifier)) {
				found = true;
			}
		}

		return found;
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	public Iterator iterator() {
		return frameMap.values().iterator();
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param tag
	 *            DOCUMENT ME!
	 */
	public void overwrite(AbstractMP3Tag tag) {
		AbstractID3v2 oldTag = this;
		AbstractID3v2 newTag = null;

		if (tag != null) {
			if (tag instanceof AbstractID3v2) {
				newTag = (AbstractID3v2) tag;
			} else {
				newTag = new ID3v2_4(tag);
			}

			Iterator iterator = newTag.frameMap.values().iterator();
			AbstractID3v2Frame frame;

			while (iterator.hasNext()) {
				frame = (AbstractID3v2Frame) iterator.next();
				oldTag.setFrame(frame);
			}
		}

		// super.overwrite(newTag);
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param file
	 *            DOCUMENT ME!
	 * 
	 * @throws IOException
	 *             DOCUMENT ME!
	 * @throws TagException
	 *             DOCUMENT ME!
	 */
	public void overwrite(RandomAccessFile file) throws IOException,
			TagException {
		AbstractID3v2 oldTag;

		try {
			oldTag = new ID3v2_4(file);
			oldTag.overwrite(this);
			oldTag.write(file);
		} catch (TagNotFoundException ex) {
			try {
				oldTag = new ID3v2_3(file);
				oldTag.overwrite(this);
				oldTag.write(file);
			} catch (TagNotFoundException ex2) {
				try {
					oldTag = new ID3v2_2(file);
					oldTag.overwrite(this);
					oldTag.write(file);
				} catch (TagNotFoundException ex3) {
					this.write(file);
				}
			}
		}
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param identifier
	 *            DOCUMENT ME!
	 */
	public void removeFrame(String identifier) {
		frameMap.remove(identifier);
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param identifier
	 *            DOCUMENT ME!
	 */
	public void removeFrameOfType(String identifier) {
		Iterator iterator = this.getFrameOfType(identifier);

		while (iterator.hasNext()) {
			AbstractID3v2Frame frame = (AbstractID3v2Frame) iterator.next();
			frameMap.remove(frame.getIdentifier());
		}
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	public java.util.Collection values() {
		return frameMap.values();
	}

	public void UpdateFromId3v1(ID3v1 id3tag) {
		if(!(this instanceof ID3v2_3)){
			write(id3tag);
			return;
		}
		// convert id3v1 tags.
		ID3v2_4Frame newFrame;
		AbstractID3v2FrameBody newBody;
		
		if (id3tag.title.length() > 0) {
			newBody = new FrameBodyTIT2((byte) 0, id3tag.title);
			newFrame = new ID3v2_4Frame(false, false, false, false, false,
					false, newBody);
			frameMap.put(newFrame.getIdentifier(), newFrame);
		}

		if (id3tag.artist.length() > 0) {
			newBody = new FrameBodyTPE1((byte) 0, id3tag.artist);
			newFrame = new ID3v2_4Frame(false, false, false, false, false,
					false, newBody);
			frameMap.put(newFrame.getIdentifier(), newFrame);
		}

		if (id3tag.album.length() > 0) {
			newBody = new FrameBodyTALB((byte) 0, id3tag.album);
			newFrame = new ID3v2_4Frame(false, false, false, false, false,
					false, newBody);
			frameMap.put(newFrame.getIdentifier(), newFrame);
		}

		if (id3tag.year.length() > 0) {
			newBody = new FrameBodyTDRC((byte) 0, id3tag.year);
			newFrame = new ID3v2_4Frame(false, false, false, false, false,
					false, newBody);
			frameMap.put(newFrame.getIdentifier(), newFrame);
		}

		if (id3tag.comment.length() > 0) {
			newBody = new FrameBodyCOMM((byte) 0, "ENG", "", id3tag.comment);
			newFrame = new ID3v2_4Frame(false, false, false, false, false,
					false, newBody);
			frameMap.put(newFrame.getIdentifier(), newFrame);
		}

		if (id3tag.genre >= 0) {
			String genre = "(" + Byte.toString(id3tag.genre) + ") "
					+ TagConstant.genreIdToString.get(new Long(id3tag.genre));
			newBody = new FrameBodyTCON((byte) 0, genre);
			newFrame = new ID3v2_4Frame(false, false, false, false, false,
					false, newBody);
			frameMap.put(newFrame.getIdentifier(), newFrame);
		}

		if (id3tag instanceof ID3v1_1) {
			ID3v1_1 id3tag2 = (ID3v1_1) id3tag;

			if (id3tag2.track > 0) {
				newBody = new FrameBodyTRCK((byte) 0, Byte
						.toString(id3tag2.track));
				newFrame = new ID3v2_4Frame(false, false, false, false, false,
						false, newBody);
				frameMap.put(newFrame.getIdentifier(), newFrame);
			}
		}
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param tag
	 *            DOCUMENT ME!
	 */
	public void write(AbstractMP3Tag tag) {
		AbstractID3v2 oldTag = this;
		AbstractID3v2 newTag = null;

		if (tag != null) {
			if (tag instanceof AbstractID3v2) {
				newTag = (AbstractID3v2) tag;
			} else {
				newTag = new ID3v2_4(tag);
			}

			Iterator iterator = newTag.frameMap.values().iterator();
			oldTag.frameMap.clear();

			AbstractID3v2Frame frame;

			while (iterator.hasNext()) {
				frame = (AbstractID3v2Frame) iterator.next();
				oldTag.setFrame(frame);
			}
		}

		// super.write(newTag);
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param buffer
	 *            DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	protected int byteArrayToSize(byte[] buffer) {
		/**
		 * the decided not to use the top bit of the 4 bytes so we need to
		 * convert the size back and forth
		 */
		return (int) (buffer[0] << 21) + (buffer[1] << 14) + (buffer[2] << 7)
				+ (buffer[3]);
	}

	/**
	 * DOCUMENT ME!
	 * 
	 * @param size
	 *            DOCUMENT ME!
	 * 
	 * @return DOCUMENT ME!
	 */
	protected byte[] sizeToByteArray(int size) {
		byte[] buffer = new byte[4];

		buffer[0] = (byte) ((size & 0x0FE00000) >> 21);
		buffer[1] = (byte) ((size & 0x001FC000) >> 14);
		buffer[2] = (byte) ((size & 0x00003F80) >> 7);
		buffer[3] = (byte) (size & 0x0000007F);

		return buffer;
	}
}